Email Service Standard
Overview
The email service follows a class-based pattern for consistency and maintainability. All email operations MUST use the EmailService class from @/lib/email.
Implementation
Email Service Class
**Location**: src/lib/email.ts
**Pattern**: Class-based with async methods
import { emailService } from '@/lib/email'
// ✅ GOOD: Using emailService class
await emailService.sendVerificationEmail(email, name, token)
await emailService.sendWelcomeEmail(email, name, subdomain)
await emailService.sendUpgradeConfirmation(email, planId)
await emailService.sendSupportTicketUpdateEmail(email, ticketId, status)Email Service Interface
interface SendEmailResult {
success: boolean
messageId?: string
error?: string
}
class EmailService {
async sendVerificationEmail(
email: string,
name: string,
token: string
): Promise<SendEmailResult>
async sendWelcomeEmail(
email: string,
name: string,
subdomain?: string,
password?: string
): Promise<SendEmailResult>
async sendUpgradeConfirmation(
email: string,
planId: string
): Promise<SendEmailResult>
async sendSupportTicketUpdateEmail(
email: string,
ticketId: string,
status: string
): Promise<SendEmailResult>
// Private method for sending emails
private async sendMail(
options: nodemailer.SendMailOptions
): Promise<SendEmailResult>
}Usage Patterns
Standard Usage
import { emailService } from '@/lib/email'
// Send verification email
const result = await emailService.sendVerificationEmail(
'user@example.com',
'John Doe',
'verification-token-123'
)
if (!result.success) {
console.error('Failed to send email:', result.error)
}Error Handling
const result = await emailService.sendWelcomeEmail(
email,
name,
subdomain,
password
)
if (!result.success) {
// Log error but don't block user flow
console.error('Welcome email failed:', result.error)
// Continue with user registration
}Async/Await
All email methods are async and return Promise<SendEmailResult>:
// ✅ GOOD: Await the result
const result = await emailService.sendVerificationEmail(...)
// ❌ BAD: Don't fire and forget (unless intentional)
emailService.sendVerificationEmail(...) // No await!Transport Configuration
The email service automatically selects the best transporter:
- **AWS SES** (Primary): Uses AWS SES if credentials are available
- **SMTP** (Fallback): Uses SMTP if SES is not configured
Configuration Priority
// AWS SES Configuration
const sesConfig = {
accessKeyId: process.env.SES_AWS_ACCESS_KEY_ID || process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.SES_AWS_SECRET_ACCESS_KEY || process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION || 'us-east-1'
}
// SMTP Fallback Configuration
const smtpConfig = {
host: process.env.EMAIL_HOST,
port: process.env.EMAIL_PORT || '587',
secure: process.env.EMAIL_SECURE === 'true',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD
}
}Best Practices
1. Always Use the Class
// ✅ GOOD: Using emailService class
import { emailService } from '@/lib/email'
await emailService.sendWelcomeEmail(email, name)
// ❌ BAD: Direct nodemailer usage
import nodemailer from 'nodemailer'
const transporter = nodemailer.createTransport(...)
await transporter.sendMail(...) // Don't do this2. Handle Errors Gracefully
// ✅ GOOD: Handle errors without blocking
const result = await emailService.sendVerificationEmail(...)
if (!result.success) {
console.error('Email failed:', result.error)
// Continue with user flow
}
// ❌ BAD: Block user flow on email failure
if (!result.success) {
throw new Error('Failed to send email') // Don't block users!
}3. Use Descriptive Subjects
// ✅ GOOD: Clear subject
await emailService.sendMail({
subject: 'Welcome to ATOM - Your Account is Ready',
html: '...'
})
// ❌ BAD: Vague subject
await emailService.sendMail({
subject: 'Email',
html: '...'
})4. Include HTML and Text
// ✅ GOOD: Both HTML and text
await emailService.sendMail({
html: '<h1>Welcome</h1>',
text: 'Welcome'
})
// ⚠️ ACCEPTABLE: HTML only (with fallback)
await emailService.sendMail({
html: '<h1>Welcome</h1>'
})Migration Guide
Current State
✅ **Already Compliant**: The email service is already implemented as a class with no standalone functions.
No migration needed! All code should use the emailService class instance.
Usage
// Import the singleton instance
import { emailService } from '@/lib/email'
// Use it in your code
await emailService.sendVerificationEmail(email, name, token)Testing
Mock Email Service
// In tests, mock the email service
jest.mock('@/lib/email', () => ({
emailService: {
sendVerificationEmail: jest.fn().mockResolvedValue({
success: true,
messageId: 'test-message-id'
})
}
}))Test Email Sending
test('sends verification email', async () => {
const result = await emailService.sendVerificationEmail(
'test@example.com',
'Test User',
'token-123'
)
expect(result.success).toBe(true)
expect(result.messageId).toBeDefined()
})Monitoring
Track Email Metrics
- Delivery success rate
- Delivery failures by type
- Message IDs for tracking
- Error rates by provider
Logging
The email service logs:
- Successful sends with message ID
- Failed sends with error details
- Transporter selection (SES vs SMTP)
Security
Environment Variables
Required environment variables:
# AWS SES (Primary)
SES_AWS_ACCESS_KEY_ID=xxx
SES_AWS_SECRET_ACCESS_KEY=xxx
AWS_REGION=us-east-1
# SMTP Fallback
EMAIL_HOST=smtp.example.com
EMAIL_PORT=587
EMAIL_SECURE=false
EMAIL_USER=user@example.com
EMAIL_PASSWORD=password
# From Address
EMAIL_FROM=noreply@atom.ai
SES_SENDER_EMAIL=noreply@atom.aiDon't Expose Credentials
// ✅ GOOD: Use environment variables
const transporter = nodemailer.createTransport({
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD
}
})
// ❌ BAD: Hardcoded credentials
const transporter = nodemailer.createTransport({
auth: {
user: 'user@example.com', // NEVER hardcode!
pass: 'password' // NEVER hardcode!
}
})References
- Implementation:
src/lib/email.ts - Nodemailer Docs: https://nodemailer.com/
- AWS SES Docs: https://docs.aws.amazon.com/ses/
Changelog
- 2026-02-08: Standard documented (already implemented as class)
- 2026-02-08: No migration needed - already compliant